/*
 * 
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 */
package commandhandlers;

import input.IRCCommand;
import interfaces.CommandHandler;

import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import main.CommonMethods;
import main.UserCentral;

import org.jibble.pircbot.Colors;

import output.IRCOutput;
import utopiaintelpackage.sos.SoS;
import utopiaintelpackage.sot.SoT;
import utopiaintelpackage.survey.Survey;
import botobjects.Province;
import botobjects.Race;

/**
 * This class is used to perform various calculations
 * 
 * 
 */
public class CalculationHandler extends CommandHandler {
   private static CalculationHandler INSTANCE = null;

   private static int calcDraftTime(final String provname, final double goaldraft, final double draftspeed, final int... args) {
	  int out = -1;
	  int totalpop = 0;
	  int peasants = 0;
	  int wizards = 0;
	  if (args.length != 3) {
		 Province prov = Province.getProvince(provname);
		 if (prov == null)
			return out;
		 SoT sot = prov.getSot();
		 if (sot == null)
			return out;
		 totalpop = sot.getDspecs() + sot.getOspecs() + sot.getSoldiers() + sot.getPeasants() + sot.getThieves()
			   + sot.getWizards() + sot.getElites();
		 peasants = sot.getPeasants();
		 wizards = sot.getWizards();
	  } else {
		 // args should consist of total pop, peasants and wizards, in that order
		 totalpop = args[0];
		 peasants = args[1];
		 wizards = args[2];
	  }
	  int todraft = (int) (totalpop * goaldraft - (totalpop - peasants - wizards));
	  if (todraft > peasants || todraft < 0)
		 return out;
	  out = (int) Math.ceil(Math.log(1 - todraft * 1.0 / peasants) / Math.log(1 - draftspeed));
	  return out;
   }

   /**
    * Calculates gains for tm's, plunders and learns
    */
   public static Double calcGains(final double target, final double self, final double total, final String type) {
	  double constant = 1.0;
	  double resources = total;
	  double gain = 0.0;
	  if ("plunder".equals(type)) {
		 constant = 0.675 / 0.12;
	  } else if ("learn".equals(type)) {
		 constant = 0.09375 / 0.12;
	  }
	  if (total == 0) {
		 resources = 1.0;
	  }
	  if (target / self < 0.9 && target / self > 0) {
		 gain = Math.max(0.0, (0.12 - (0.9 - target / self) * 0.36) * constant * resources);
	  } else if (target / self >= 0.9 && target / self <= 1.1) {
		 gain = 0.12 * constant * resources;
	  } else if (target / self > 1.1) {
		 gain = Math.max(0.0, (0.12 - (target / self - 1.1) * 0.206) * constant * resources);
	  }
	  return gain;
   }

   public static int calcWizards(final String province) {
	  int out = 0;
	  Province prov = Province.getProvince(province);
	  SoT sot = prov.getSot();
	  Race race = Race.getRace(prov.getRace());
	  if (race == null || sot == null || !sot.getInfiltrated() || prov.getNw() == 0)
		 return out;

	  int nw = prov.getNw();
	  nw -= sot.getThieves() * 4;
	  nw -= sot.getSoldiers() * 1.5*(race.getRace().equals("Halfling")?2:1);
	  nw -= sot.getPeasants() * 1;
	  nw -= sot.getDspecs() * race.getDspecstr();
	  nw -= sot.getOspecs() * race.getOspecstr() * 0.8;
	  nw -= sot.getElites() * race.getElitenw();
	  nw -= sot.getHorses() * 0.6;
	  nw -= sot.getMoney() / 1000.0;

	  SoS sos = prov.getSos();
	  nw -= sos == null ? sot.getLand() * 300 / 92.0 : sos.getTotalSciencePoints() / 92.0;
	  Survey survey = prov.getSurvey();
	  nw -= survey == null ? sot.getLand() * 55 : survey.getTotalbuilt() * 55 + survey.getTotalprogress() * 30
			+ (survey.getLand() - survey.getTotalbuilt() - survey.getTotalprogress()) * 15;
	  out = Math.max(0, nw / 4);
	  return out;
   }

   public static final CalculationHandler getInstance() {
	  if (INSTANCE == null) {
		 INSTANCE = new CalculationHandler();
	  }
	  return INSTANCE;
   }

   public static final CalculationHandler newInstance() {
	  INSTANCE = null;
	  return getInstance();
   }

   private final List<String> opstack;

   private CalculationHandler() {
	  opstack = new ArrayList<String>();
   }

   /**
    * Calculates an arithmetic expression
    * 
    * @param in
    * @return String with the result
    */
   public String calc(final String in) {
	  try {
		  String formatted = formatExpression(in);
		  String ops = "(/*+-^";
		  String temp = "";
		  Integer i = 0;

		  while (true) {
			 while (i < formatted.length()) {
				if (ops.indexOf(formatted.charAt(i)) != -1) {
				   if (!"".equals(temp)) {
					  opstack.add(temp);
					  temp = "";
				   }
				   opstack.add(Character.toString(formatted.charAt(i)));
				} else if (formatted.charAt(i) == ')') {
				   if (!"".equals(temp)) {
					  opstack.add(temp);
					  temp = "";
				   }
				   ++i;
				   break;
				} else {
				   temp = temp + formatted.charAt(i);
				}
				++i;
			 }
			 if (!"".equals(temp)) {
				opstack.add(temp);
				temp = "";
			 }

			 if (!calcPart())
				return "";
			 if (opstack.size() == 1) {
				break;
			 }
		  }
		  if ("".equals(formatted)) {
			 formatted = " ";
		  }
		  Double d = Double.parseDouble(opstack.get(0));
		  String out = "";
		  NumberFormat nf = NumberFormat.getNumberInstance(Locale.US);
		  nf.setGroupingUsed(true);
		  nf.setMaximumFractionDigits(5);
		  nf.setMinimumFractionDigits(0);
		  out = formatted.substring(1, formatted.length() - 1) + " =" + Colors.RED + " " + nf.format(d);

		  opstack.clear();
		  return out;
	  } catch (Exception e) {
		 e.printStackTrace();
	  }
	  return "";
   }

   /**
    * Calculates a part of the expression (one paranthesis)
    */
   private boolean calcPart() {
	  try {
		 int pos = 0;
		 for (int i = 0; i < opstack.size(); ++i) {
			if (opstack.get(i).equals("(")) {
			   pos = i;
			}
		 }
		 Double result = null;
		 int i = pos;
		 while (checkIfDone(pos, "^", "^") && i < opstack.size()) {
			if (opstack.get(i).equals("^")) {
			   result = Math.pow(Double.parseDouble(opstack.get(i - 1)), Double.parseDouble(opstack.get(i + 1)));
			   opstack.set(i - 1, result.toString());
			   opstack.remove(i);
			   opstack.remove(i);
			} else {
			   ++i;
			}
		 }
		 i = pos;
		 while (checkIfDone(pos, "*", "/") && i < opstack.size()) {
			if (opstack.get(i).equals("*")) {
			   result = Double.parseDouble(opstack.get(i - 1)) * Double.parseDouble(opstack.get(i + 1));
			   opstack.set(i - 1, result.toString());
			   opstack.remove(i);
			   opstack.remove(i);
			} else if (opstack.get(i).equals("/")) {
			   result = Double.parseDouble(opstack.get(i - 1)) / Double.parseDouble(opstack.get(i + 1));
			   opstack.set(i - 1, result.toString());
			   opstack.remove(i);
			   opstack.remove(i);
			} else {
			   ++i;
			}
		 }
		 i = pos;
		 while (checkIfDone(pos, "+", "-") && i < opstack.size()) {
			if (opstack.get(i).equals("+")) {
			   result = Double.parseDouble(opstack.get(i - 1)) + Double.parseDouble(opstack.get(i + 1));
			   opstack.set(i - 1, result.toString());
			   opstack.remove(i);
			   opstack.remove(i);
			   --i;
			} else if (opstack.get(i).equals("-")) {
			   result = Double.parseDouble(opstack.get(i - 1)) - Double.parseDouble(opstack.get(i + 1));
			   opstack.set(i - 1, result.toString());
			   opstack.remove(i);
			   opstack.remove(i);
			   --i;
			}
			++i;
		 }
		 if (opstack.get(pos).equals("(")) {
			opstack.remove(pos);
		 }
		 return true;
	  } catch (final NumberFormatException e) {
		 return false;
	  } catch (final IndexOutOfBoundsException e) {
		 return false;
	  }
   }

   /**
    * Calculates the chance of breaking someone, given the off and def. Not sure if this formula is actually correct though.
    * 
    * @param off
    * @param def
    * @return
    */
   private String chanceCalc(final int off, final int def) {
	  Double send = 1.0 * off / def;
	  Double q = 0.97 / send;
	  int mode = 1;
	  Double max = 1.035 / 0.965;
	  Double min = 0.965 / 1.035;
	  Double success;
	  if (q >= max) {
		 success = 0.0;
	  } else if (q > mode) {
		 success = Math.pow((max - q), 2) / ((max - mode) * (max - min));
	  } else if (q <= min) {
		 success = 1.0;
	  } else {
		 success = 1 - Math.pow((q - min), 2) / ((mode - min) * (max - min));
	  }
	  NumberFormat nf = NumberFormat.getNumberInstance(Locale.US);
	  nf.setMinimumFractionDigits(1);
	  nf.setMaximumFractionDigits(4);
	  return "Chance to succeed = " + Colors.RED + nf.format(success * 100) + "%";
   }

   /**
    * Checks if the calculation is finished or not
    * 
    * @param pos
    * @param sign1
    * @param sign2
    * @return true if the calculation is finished, false if not
    */
   private boolean checkIfDone(final int pos, final String sign1, final String sign2) {
	  int temppos = pos;
	  if (pos == -1)
		 return true;
	  for (int i = temppos; i < opstack.size(); ++i) {
		 if (opstack.get(i).equalsIgnoreCase(sign1) || opstack.get(i).equalsIgnoreCase(sign2))
			return true;
	  }
	  return false;
   }

   private static final Pattern multiplyInsertPattern1=Pattern.compile("\\)(\\d)");
   private static final Pattern multiplyInsertPattern2=Pattern.compile("(\\d)\\(");
   private static final Pattern multiplyInsertPattern3=Pattern.compile("\\)\\(");
   
   /**
    * Formats the expression in a way that makes it easier to do calculations on Removes unsupported characters
    * 
    * @param expression
    * @return String with a formatted expression
    */
   private String formatExpression(String expression) {
	  opstack.clear();
	  String allowed = "1234567890./*+-()^";
	  String formatted = "(";
	  for (int i = 0; i < expression.length(); ++i) {
		 if (allowed.indexOf(expression.charAt(i)) != -1) {
			formatted = formatted + expression.charAt(i);
		 }
	  }
	  formatted += ")";
	  formatted = formatted.replace("(-", "(0-");
	  Matcher matcher=multiplyInsertPattern1.matcher(formatted);
	  formatted=matcher.replaceAll(")*$1");
	  matcher=multiplyInsertPattern2.matcher(formatted);
	  formatted=matcher.replaceAll("$1*(");
	  matcher=multiplyInsertPattern3.matcher(formatted);
	  formatted=matcher.replaceAll(")*(");
	  return formatted;
   }

   @Override
   public void handle(final IRCCommand command, final IRCOutput out) {
   }

   public void handleCalc(final IRCCommand command, final IRCOutput out) {
	  String s = "";
	  for (int i = 1; i < command.size(); ++i) {
		 s += command.get(i);
	  }
	  out.addOutput(calc(s));
   }

   public void handleChance(final IRCCommand command, final IRCOutput out) {
	  if (command.size() == 3 && command.isInt(1) && command.isInt(2)) {
		 out.addOutput(chanceCalc(Integer.parseInt(command.get(1)), Integer.parseInt(command.get(2))));
	  }
   }

   public void handleDraft(final IRCCommand command, final IRCOutput out) {
	  if (command.size() == 3 && command.isDouble(1) && command.isDouble(2)) {
		 String prov = CommonMethods.getProv(UserCentral.getInstance().getOnlineUser(command.getSender()));
		 if (!"".equals(prov)) {
			int hours = calcDraftTime(prov, Double.parseDouble(command.get(1)), Double.parseDouble(command.get(2)));
			if (hours != -1) {
			   out.addOutput("You need to draft for " + hours + " hours");
			}
		 }
	  } else if (command.size() == 6 && command.isDouble(1) && command.isDouble(2) && command.isInt(3) && command.isInt(4)
			&& command.isInt(5)) {
		 int hours = calcDraftTime("", Double.parseDouble(command.get(1)), Double.parseDouble(command.get(2)),
			   Integer.parseInt(command.get(3)), Integer.parseInt(command.get(4)), Integer.parseInt(command.get(5)));
		 if (hours != -1) {
			out.addOutput("You need to draft for " + hours + " hours");
		 }
	  }
   }

   public void handleGain(final IRCCommand command, final IRCOutput out) {
	  Double gain = null;
	  for (int i = 1; i < command.size(); ++i) {
		 command.set(i, command.get(i).replace(",", ""));
	  }
	  if (command.size() == 3) {
		 if (command.isInt(1) && command.isInt(2)) {
			double target = Double.parseDouble(command.get(1));
			double self = Double.parseDouble(command.get(2));
			gain = calcGains(target, self, 0, "tm");
		 }
	  } else if (command.size() == 4) {
		 if (command.isInt(1) && command.isInt(2) && command.isInt(3)) {
			double target = Double.parseDouble(command.get(1));
			double self = Double.parseDouble(command.get(2));
			double land = Double.parseDouble(command.get(3));
			gain = calcGains(target, self, land, "tm");
		 } else if (command.get(1).equalsIgnoreCase("plunder") && command.isInt(2) && command.isInt(3)) {
			double target = Double.parseDouble(command.get(2));
			double self = Double.parseDouble(command.get(3));
			gain = calcGains(target, self, 0, "plunder");
		 } else if (command.get(1).equalsIgnoreCase("learn") && command.isInt(2) && command.isInt(3)) {
			double target = Double.parseDouble(command.get(2));
			double self = Double.parseDouble(command.get(3));
			gain = calcGains(target, self, 0, "learn");
		 }
	  } else if (command.size() == 5) {
		 if (command.get(1).equalsIgnoreCase("plunder") && command.isInt(2) && command.isInt(3) && command.isInt(4)) {
			double target = Double.parseDouble(command.get(2));
			double self = Double.parseDouble(command.get(3));
			double gold = Double.parseDouble(command.get(4));
			gain = calcGains(target, self, gold, "plunder");
		 } else if (command.get(1).equalsIgnoreCase("learn") && command.isInt(2) && command.isInt(3) && command.isInt(4)) {
			double target = Double.parseDouble(command.get(2));
			double self = Double.parseDouble(command.get(3));
			double sci = Double.parseDouble(command.get(4));
			gain = calcGains(target, self, sci, "learn");
		 }
	  }
	  if (gain == null)
		 return;
	  String result = "";
	  if (command.size() == 4 && command.isInt(1) || command.size() == 5) {
		 result = String.valueOf(gain.intValue());
	  } else {
		 NumberFormat nf = NumberFormat.getNumberInstance(Locale.US);
		 nf.setGroupingUsed(true);
		 nf.setMaximumFractionDigits(3);
		 nf.setMinimumFractionDigits(0);
		 result = nf.format(gain * 100.0) + "%";
	  }
	  out.addOutput(Colors.BLUE + "Gain:" + Colors.RED + " " + result);
   }

   public void handleKdgain(final IRCCommand command, final IRCOutput out) {
	  Double gainFactor = null;
	  boolean withGain = false;
	  for (int i = 1; i < command.size(); ++i) {
		 command.set(i, command.get(i).replace(",", ""));
	  }
	  if (command.size() == 3) {
		 if (command.isInt(1) && command.isInt(2)) {
			double target = Double.parseDouble(command.get(1));
			double self = Double.parseDouble(command.get(2));
			if (target / self < 0.9 && target / self > 0) {
			   gainFactor = Math.max(2.0 / 3, 1 - 0.67 * (0.9 - target / self));
			} else if (target / self >= 0.9) {
			   gainFactor = 1.0;
			}
		 }
	  } else if (command.size() == 4) {
		 if (command.isInt(1) && command.isInt(2) && (command.isInt(3) || command.isDouble(3))) {
			double target = Double.parseDouble(command.get(1));
			double self = Double.parseDouble(command.get(2));
			double calcedGain = Double.parseDouble(command.get(3));
			withGain = true;
			if (target / self < 0.9 && target / self > 0) {
			   gainFactor = Math.max(2.0 / 3 * calcedGain, (1 - 0.67 * (0.9 - target / self)) * calcedGain);
			} else if (target / self >= 0.9) {
			   gainFactor = calcedGain;
			}
		 }
	  }
	  if (gainFactor == null)
		 return;
	  NumberFormat nf = NumberFormat.getNumberInstance(Locale.US);
	  nf.setGroupingUsed(true);
	  nf.setMaximumFractionDigits(3);
	  nf.setMinimumFractionDigits(0);
	  out.addOutput(Colors.BLUE + "Gain:"
			+ (withGain ? Colors.RED + " " + gainFactor.intValue() : Colors.RED + " raw gain*" + nf.format(gainFactor)));
   }

   public void handleRange(final IRCCommand command, final IRCOutput out) {
	  if (command.size() > 1) {
		 for (int i = 1; i < command.size(); ++i) {
			if (!command.isInt(i))
			   return;
		 }
		 Double min = 0.0;
		 Double max = Double.MAX_VALUE;
		 for (int i = 1; i < command.size(); ++i) {
			min = Math.max(Integer.parseInt(command.get(i)) / 1.03, min);
			max = Math.min(Integer.parseInt(command.get(i)) / 0.97, max);
		 }
		 if (min <= max) {
			out.addOutput(Colors.OLIVE + "Range is: " + Colors.BLUE + min.intValue() + " - " + max.intValue());
		 } else {
			out.addOutput(Colors.OLIVE + "Range is too big");
		 }
	  }
   }

   public void handleWpacalc(final IRCCommand command, final IRCOutput out) {
	  if (command.size() > 1) {
		 String prov = CommonMethods.provinceExists(command.getFullMessage());
		 if (!"".equals(prov) && Province.updateWizards(prov, CalculationHandler.calcWizards(prov))) {
			out.addOutput("Amount of wizards calced and updated for " + prov);
		 }
	  }
   }
}
